Skip to content

fix(sdk/go): use the CLI oidc_token.json format - #4830

Merged
TaylorMutch merged 1 commit into
NVIDIA:mainfrom
lunarwhite:fix/4508-go-sdk-oidc-token-format/lunarwhite
Nov 3, 2026
Merged

TaylorMutch merged 1 commit into
NVIDIA:mainfrom
lunarwhite:fix/4508-go-sdk-oidc-token-format/lunarwhite

Conversation

@lunarwhite

@lunarwhite lunarwhite commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Go SDK now reads and writes oidc_token.json in the CLI's format (expires_at, issuer, client_id), so one login works for both the CLI and Go programs. gateway.NewClient stops sending an expired token and fails with a run `openshell gateway login <name>` hint instead.

Related Issue

Closes #4508

Changes

  • One internal oidctoken.Bundle mirrors the CLI's OidcTokenBundle and replaces the two private Go structs.
  • gateway rejects token files that are expired or lack issuer or client_id, including files from earlier Go SDK builds. Every load error carries the login hint.
  • oidc.Login writes the CLI format and reuses a stored token only for the same issuer and client ID.
  • Behavior change: v1.RefreshableToken no longer returns an expired cached token after a failed refresh. It returns the refresh error, which surfaces as Unauthenticated. This applies to every token source and matches the CLI.
  • New shared fixture sdk/conformance/oidc-token-bundle.json. A Rust test checks it against the CLI's serialization, and Go tests check reading and writing it.

Not covered: Go Login still ignores the gateway's oidc_scopes and oidc_audience (#4513). Refresh and file locking stay in #4514.

Release Notes

The Go SDK now shares oidc_token.json with the CLI, and v1.RefreshableToken stops returning an expired cached token for any token source; run openshell gateway login once to replace token files from earlier releases.

Testing

  • Checks appropriate to the affected code and behavior pass: mise run go:ci, cargo test -p openshell-bootstrap --lib oidc_token, cargo fmt and cargo clippy for openshell-bootstrap, and mise run license:check
  • Unit tests added/updated
  • E2E tests added/updated (if applicable)

Key tests: the shared fixture in Rust and Go, the issue reproduction (an expired CLI-written file fails with the hint), and RefreshableToken failing closed during backoff.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

@copy-pr-bot

copy-pr-bot Bot commented Oct 22, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@lunarwhite

Copy link
Copy Markdown
Contributor Author

Hi @TaylorMutch, would you please take a look at this PR when you have a moment? I'd like to hear your thoughts as you triaged it. Thanks in advance!

@lunarwhite
lunarwhite force-pushed the fix/4508-go-sdk-oidc-token-format/lunarwhite branch from b752b4f to 86d7f48 Compare October 24, 2026 12:36

@rhuss rhuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc-review Summary

What Went Well

  • Genuine cross-language anti-drift: the single fixture sdk/conformance/oidc-token-bundle.json is asserted from both Rust (oidc_token.rs:197) and Go (read oidc/token_test.go:28, write :43), so a field change on either side breaks that language's test.
  • internal/oidctoken centralizes the on-disk Bundle type and converters, removing the two divergent private structs (the root-cause fix for #4508).
  • Missing/null expires_at is handled consistently as non-expiring on every path (Go and Rust), with dedicated nil-vs-zero tests.
  • The RefreshableToken backoff path is nil-error safe: nextRetry/lastErr are set together on failure and cleared together on success.
  • Go token persistence is atomic and 0600 (CreateTemp + Chmod + Sync + Rename) with temp-file cleanup on every error path.

Findings

Severity File Description Source
Notable sdk/go/openshell/v1/auth_refresh.go:140 Fail-closed RefreshableToken change applies to every token source goal-alignment
Notable sdk/go/openshell/v1/gateway/token.go:120 Upgrade invalidates older Go-SDK-written token files (missing issuer/client_id) production
Notable sdk/go/openshell/v1/gateway/token.go:120 Runtime path does not match stored issuer/client_id against gateway config security
Minor sdk/go/openshell/v1/gateway/token.go:120 Load-bearing issuer/client_id guard is undocumented gator
Minor crates/openshell-bootstrap/src/oidc_token.rs:54 Rust writes oidc_token.json non-atomically (perm window + partial-read race) security
Notable sdk/go/openshell/v1/auth_refresh.go:82 No-expiry cached token is never re-read (won't pick up a later expiring token) gator

Two findings are summary-only because they fall on lines outside this PR's diff:

  • crates/openshell-bootstrap/src/oidc_token.rs:54 (Minor, pre-existing store_oidc_token): non-atomic std::fs::write + separate chmod 0600 leaves a brief wider-permission window and a truncating in-place write that the Go reader can observe mid-write. The Go writer this PR adds does it correctly. In the issue's stated scope; mitigated by the 0700 parent dir and the cached-token fallback.
  • sdk/go/openshell/v1/auth_refresh.go:82 (Notable): a no-expiry cached token makes isTokenValid() always true, so a long-lived process never re-reads the file and won't pick up a later CLI-written expiring token until restart. Consistent with the Rust side; the only defensible behavior without expiry metadata.

Review Details

  • Findings posted: 6 (4 inline, 2 summary-only)
  • Gate outcome: PASS (0 Critical, 0 Important)
  • Participating reviewers: goal-alignment, production, security, gator (architecture, correctness, test-quality ran with no surviving findings; codex and coderabbit skipped on this fork PR)

Comment thread sdk/go/openshell/v1/auth_refresh.go Resolved
Comment thread sdk/go/openshell/v1/gateway/token.go Resolved
Comment thread sdk/go/openshell/v1/gateway/token.go Resolved
Comment thread sdk/go/openshell/v1/gateway/token.go Resolved
@lunarwhite

Copy link
Copy Markdown
Contributor Author

Re: review comments in the body

  • crates/openshell-bootstrap/src/oidc_token.rs:54 (Minor, pre-existing store_oidc_token)

I'll keep this for #4514 with the locking. I checked it's a small tempfile + persist change, so it'll fit there easily.

  • sdk/go/openshell/v1/auth_refresh.go:82 (Notable)

Re-reading those breaks TestGetRequestMetadata_ZeroExpiryNeverRefreshes and would call Token() on every RPC for non-expiring sources, so I'd leave this as-is.

@lunarwhite
lunarwhite force-pushed the fix/4508-go-sdk-oidc-token-format/lunarwhite branch from 86d7f48 to 6054443 Compare October 27, 2026 10:53
@lunarwhite

Copy link
Copy Markdown
Contributor Author

Appreciate your review @rhuss! I've replied to each review comment from my perspective. Please let me know what you think. I'm open to addressing your feedback, as you write the Go sdk mostly :)

@rhuss rhuss left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's is another round of reviews on the latest changes (with updated agents, so there might be findings that the first round hasn't found). Although there are some findings (hopefully easy to fix by aganets), I think we are close to converge.


cc-review Summary

New round with fresh agent eyes on 6054443. The earlier threads are either addressed or continue in their existing threads (including the open issuer/client_id matching question), so this round lists only new observations. The findings come from an agent-assisted review; the failure scenarios were derived from the code and not executed.

What Went Well

  • sdk/go/openshell/v1/internal/oidctoken/oidctoken.go: one internal Bundle type with FromToken/Token replaces the two divergent oidcBundle copies in gateway/token.go and oidc/token.go. Its field tags (omitempty on refresh_token, *uint64 expires_at, required issuer/client_id) mirror the serde attributes of OidcTokenBundle in openshell-bootstrap.
  • sdk/conformance/oidc-token-bundle.json is checked from both sides: Rust conformance_fixture_matches_cli_serialization (crates/openshell-bootstrap/src/oidc_token.rs) and Go TestWriteToken_MatchesConformanceFixture and TestReadToken_ConformanceFixture, so a format drift between CLI and SDK fails CI.
  • sdk/go/openshell/v1/gateway/token.go diskTokenSource: bundles without issuer/client_id are rejected instead of being treated as never-expiring, the expiry check fails closed at the exact expiry instant, and loadError gives every failure branch the ErrTokenLoad wrap and the openshell gateway login <name> hint without putting token material in the message.
  • sdk/go/openshell/v1/auth_refresh.go usableCached/lastErr: after a failed refresh, an expired cached bearer is no longer sent, and the backoff path returns the real refresh error. lastErr is read and written under r.mu and cleared on success. TestGetRequestMetadata_RefreshFailureWithExpiredCachedTokenReturnsError covers it with require.ErrorIs, a check that no "using cached token" log was emitted, and a source call count.
  • sdk/go/openshell/v1/oidc/oidc.go Login reuses a stored token only when the normalized issuer and the client ID match, and TestLogin_ReplacesMismatchedStoredToken covers the client-id, issuer, and legacy-format cases in one table.
  • TestDiskTokenSource_ExpiredToken and TestDiskTokenSource_NoExpiry table-test the boundary values expires_at=0, an absent field, and explicit null.

Findings

Severity File Description Source
Minor sdk/go/openshell/v1/auth_refresh.go:102 During backoff the cached lastErr is replayed, so a fresh login is not picked up for up to 30s gator
Minor sdk/go/openshell/v1/gateway/gateway_test.go:93 TestNewClient_AuthModeOIDC seeds a token file in a format the SDK now rejects test-quality
Minor sdk/go/openshell/v1/gateway/gateway_test.go:345 No gateway-level test for the ErrTokenLoad plus login hint path through resolveAuthProvider test-quality
Minor sdk/go/openshell/v1/oidc/oidc_test.go:204 TestLogin_ReplacesMismatchedStoredToken asserts only access_token on disk test-quality
Minor sdk/go/docs/src/api/gateway.md:160 ErrTokenLoad row and related comments do not mention expired tokens architecture
Notable sdk/go/openshell/v1/gateway/token.go:107 Reading and parsing oidc_token.json is still duplicated in the gateway and oidc packages architecture

Not Posted Inline

sdk/go/docs/src/api/gateway.md:160: Location is outside the PR diff (line 160).

Minor: The ErrTokenLoad row in the error table still says "Token file missing or unreadable", and so does gateway/doc.go:64. The new paragraph right above the table (lines 151-154) lists a missing, malformed, or expired oidc_token.json, and the comment in gateway/errors.go:19-20 says "missing, unreadable, or malformed". None of these reference spots mention expired tokens.

Why this matters: The table introduces errors.Is classification, so a reader could wrap an RPC in errors.Is(err, gateway.ErrTokenLoad) to catch an expired OIDC token. That check never matches: the sentinel comes from GetRequestMetadata, gRPC turns it into a status, and the SDK StatusError does not keep the wrap chain. The paragraph above already points to v1.IsUnauthenticated, so this is about keeping the reference row in line with it. Expired tokens returned ErrTokenLoad before this PR too, so the wording gap is older than this change.

Suggested fix: In the table and in doc.go, either drop ErrTokenLoad or describe it as "returned by the auth provider's GetRequestMetadata for missing, malformed, expired, or pre-CLI-format token files; on RPCs it surfaces as Unauthenticated". Add the expired case to the errors.go comment.

Source: architecture agent

Review Details

  • Findings posted: 6
  • Findings reviewed and not posted: 1
  • Gate outcome: PASS
  • Participating reviewers: architecture, gator, production, test-quality

Comment thread sdk/go/openshell/v1/auth_refresh.go Resolved
Comment thread sdk/go/openshell/v1/gateway/token.go Resolved
Comment thread sdk/go/openshell/v1/oidc/oidc_test.go Resolved
Signed-off-by: Yuedong Wu <dwcn22@outlook.com>
@lunarwhite
lunarwhite force-pushed the fix/4508-go-sdk-oidc-token-format/lunarwhite branch from 6054443 to edfcb47 Compare October 29, 2026 06:02
@rhuss

rhuss commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Heads-up: the Go SDK moves to ai-openshell/sdk-go on 2026-11-03 (details in #5031). This PR also changes crates/openshell-bootstrap, scripts/, and sdk/conformance/, which stay in core. If it is still open at the freeze, only the sdk/go part is ported to the new repository with your commits and authorship kept, and the core part stays here for review. A comment here links the ported PR.

@lunarwhite

Copy link
Copy Markdown
Contributor Author

Hi @rhuss, when you get a chance, could you take a relook at this PR? I think we're close to merge. Thanks for the heads-up!

@purp

purp commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

/ok to test edfcb47

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Compatibility review (advisory)

New or updated breaking changes; see the full report for all findings.

1. state — No explicit risk acceptance found

oidc_token.json files written by the baseline Go SDK were accepted, but the candidate rejects them because they lack issuer and client_id.

  • Impact: Existing unexpired Go-provisioned sessions fail authentication through gateway.NewClient, while oidc.Login starts interactive authentication instead of reusing their cached tokens.
  • Action: For v0.1.7, preserve legacy expiry parsing and transparently migrate valid Go-written bundles rather than requiring openshell gateway login &lt;name&gt;.
  • PR: fix(sdk/go): use the CLI oidc_token.json format #4830

2. go-sdk — No explicit risk acceptance found

v1.RefreshableToken previously returned the cached bearer after refresh failures regardless of expiry, but now returns the refresh error once the cached token expires.

  • Impact: Consumers of any oauth2.TokenSource relying on stale-token fallback during provider outages now receive authentication errors without sending their RPCs.
  • Action: Preserve RefreshableToken's existing default in v0.1.7 and make fail-closed behavior opt-in, or defer the default change to a minor train with actionable migration guidance.
  • PR: fix(sdk/go): use the CLI oidc_token.json format #4830
Review details
  • Execution: complete
  • Candidate: PR #4830 (541b688789da5c58313342f7373a863dbfb11add)
  • Baseline: d4e32263eb4554c46c5bae702acada2f4af03eb0 (d4e32263eb4554c46c5bae702acada2f4af03eb0)
  • Reviewed: rust-sdk, python-sdk, go-sdk, typescript-sdk, api-behavior, cli, configuration, policy, helm, state

Advisory review; publication gates are unchanged.

Branch check run

@rhuss

rhuss commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

I'm not fully sure about the pre-condition for when a E2E tests run:

  • /ok to test by a maintainer
  • ???

What does this 3 workflows awaiting approval ? Is there an extra step required to get it running ?

@rhuss

rhuss commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up on my review: I checked the current head (edfcb47) against my open threads.

  • Fixed: TestNewClient_AuthModeOIDC now seeds a CLI-format bundle, TestResolveAuthProvider_OIDCLegacyFileReturnsLoginHint checks ErrTokenLoad plus the login hint on the first call and again during backoff, and TestLogin_ReplacesMismatchedStoredToken asserts the rewritten issuer and client ID.
  • Deferred to bug(sdk/go): stored refresh token is never used; expired logins always fall back to interactive login #4514: re-reading oidc_token.json during the refresh backoff, and moving the read and write into internal/oidctoken. Both fit the locking work planned there.

Nothing blocking from my side. Ready for a maintainer review.

@TaylorMutch TaylorMutch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed CLI token format compatibility, cached-login validation, expiry handling, and refresh failure backoff. No actionable findings. The affected Go packages passed with the race detector, and required CI checks are passing.

@TaylorMutch
TaylorMutch enabled auto-merge November 3, 2026 17:28
@TaylorMutch
TaylorMutch added this pull request to the merge queue Nov 3, 2026
Merged via the queue into NVIDIA:main with commit b55ad7e Nov 3, 2026
79 checks passed
@lunarwhite
lunarwhite deleted the fix/4508-go-sdk-oidc-token-format/lunarwhite branch November 3, 2026 23:08
Gkrumbach07 pushed a commit to ai-openshell/sdk-go that referenced this pull request Nov 4, 2026
…k-go (#4)

* chore(hack): add migration and automation script scaffolding

Add the hack/migration and hack/automation directories with their READMEs,
the filter-repo message rules, and the history extraction and tree
verification scripts used for the import from NVIDIA/OpenShell.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* feat(hack): add port-pr.sh to move open core PRs with their authorship

Formats the sdk/go commits of an open NVIDIA/OpenShell PR as patches,
rewrites the module path inside them, and applies them with git am on
port/<n>. Authors, dates, and the original Signed-off-by trailers are
kept; the porter's format.signoff setting is ignored. Lines a 3-way
merge leaves with the old path are rewritten, lines already in the base
are not. Pushing and opening the PR needs --push.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* chore(hack): record the freeze commit and v0.1.7 as the first core pin

Signed-off-by: Roland Huß <rhuss@redhat.com>

* refactor!: move the module path to github.com/ai-openshell/sdk-go

Rewrite every github.com/NVIDIA/OpenShell/sdk/go import, the go.mod module line, the buf M options, and the docs links.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* build: generate bindings from a pinned core tag and make mise self-contained

buf.gen.yaml reads proto/ from NVIDIA/OpenShell at the tag in inputs[0].tag (v0.1.6), so no core checkout is needed. mise.toml carries its own tools and lockfile and adds format:check and core:version. Regenerating at v0.1.6 changes no bindings.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* docs: point the README and license at the new repository

Add the go get line, a supported core versions note, and a migration section for consumers of github.com/NVIDIA/OpenShell/sdk/go. Docs links move to https://ai-openshell.github.io/sdk-go/. Copy LICENSE from core and ignore coverage.out and the built book.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* ci: run the mise CI pipeline on pull requests and main

Signed-off-by: Roland Huß <rhuss@redhat.com>

* test: vendor the shared conformance fixtures

The OAuth client-credentials and OIDC token-bundle fixtures live in
core's sdk/conformance/, which the tests reached through ../../../..
paths. Copy them next to the tests that read them, and check in
conformance:check that they match core at the pinned tag. A fixture
the pinned tag does not have yet (oidc-token-bundle.json came with
NVIDIA/OpenShell#4830 after v0.1.7) is reported and skipped until the
pin includes it.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* build: pin core v0.1.7

v0.1.7 is the stable core release at the freeze. Its proto/ equals
proto/ at the freeze commit, so the regenerated bindings do not change.

Signed-off-by: Roland Huß <rhuss@redhat.com>

---------

Signed-off-by: Roland Huß <rhuss@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(sdk/go): oidc_token.json format is incompatible with the CLI (expires_at/issuer/client_id)

4 participants